package com.zzm.easyokhttp3.http;


import okhttp3.*;
import okio.BufferedSink;

import javax.net.ssl.*;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
 * @ClassName Client
 * @Description okHttp3客户端
 * @Author zzm
 * @Date 2019/6/13
 * @Version 1.0
 */
public final class Client {
    public static final String DefaultMime = "application/octet-stream";
    public static final String JsonMime = "application/json";
    public static final String FormMime = "application/x-www-form-urlencoded";
    private final OkHttpClient httpClient;
    private MyTrustManager mMyTrustManager;

    /**
     * 构建一个默认配置的 HTTP Client 类
     */
    public Client(HttpConfiguration cfg) {
        this(null, null,
                cfg.connectTimeout, cfg.readTimeout, cfg.writeTimeout,
                cfg.dispatcherMaxRequests, cfg.dispatcherMaxRequestsPerHost,
                cfg.connectionPoolMaxIdleCount, cfg.connectionPoolMaxIdleMinutes);
    }

    public Client(HttpConfiguration cfg, ProxyConfiguration proxy) {
        this(null, proxy,
                cfg.connectTimeout, cfg.readTimeout, cfg.writeTimeout,
                cfg.dispatcherMaxRequests, cfg.dispatcherMaxRequestsPerHost,
                cfg.connectionPoolMaxIdleCount, cfg.connectionPoolMaxIdleMinutes);
    }

    /**
     * 构建一个自定义配置的 HTTP Client 类
     */
    public Client(final Dns dns, final ProxyConfiguration proxy,
                  int connTimeout, int readTimeout, int writeTimeout, int dispatcherMaxRequests,
                  int dispatcherMaxRequestsPerHost, int connectionPoolMaxIdleCount,
                  int connectionPoolMaxIdleMinutes) {
        Dispatcher dispatcher = new Dispatcher();
        dispatcher.setMaxRequests(dispatcherMaxRequests);
        dispatcher.setMaxRequestsPerHost(dispatcherMaxRequestsPerHost);
        ConnectionPool connectionPool = new ConnectionPool(connectionPoolMaxIdleCount,
                connectionPoolMaxIdleMinutes, TimeUnit.MINUTES);
        OkHttpClient.Builder builder = new OkHttpClient.Builder();

        builder.dispatcher(dispatcher);
        builder.connectionPool(connectionPool);
        builder.addNetworkInterceptor(chain -> {
            Request request = chain.request();
            okhttp3.Response response = chain.proceed(request);
            IpTag tag = null;
            try {
                tag = (IpTag) request.tag();
                tag.ip = chain.connection().socket().getRemoteSocketAddress().toString();
            } catch (Exception e) {
                e.printStackTrace();
                tag.ip = "";
            }
            return response;
        });
        if (dns != null) {
            builder.dns(hostname -> {
                try {
                    return dns.lookup(hostname);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return okhttp3.Dns.SYSTEM.lookup(hostname);
            });
        }
        if (proxy != null) {
            builder.proxy(proxy.proxy());
            if (proxy.user != null && proxy.password != null) {
                builder.proxyAuthenticator(proxy.authenticator());
            }
        }
        builder.sslSocketFactory(createSSLSocketFactory(), mMyTrustManager)
                .hostnameVerifier(new TrustAllHostnameVerifier());
        builder.connectTimeout(connTimeout, TimeUnit.SECONDS);
        builder.readTimeout(readTimeout, TimeUnit.SECONDS);
        builder.writeTimeout(writeTimeout, TimeUnit.SECONDS);
        httpClient = builder.build();
    }

    private SSLSocketFactory createSSLSocketFactory() {
        SSLSocketFactory ssfFactory = null;
        try {
            mMyTrustManager = new MyTrustManager();
            SSLContext sc = SSLContext.getInstance("TLS");
            sc.init(null, new TrustManager[]{mMyTrustManager}, new SecureRandom());
            ssfFactory = sc.getSocketFactory();
        } catch (Exception ignored) {
            ignored.printStackTrace();
        }

        return ssfFactory;
    }

    //实现X509TrustManager接口
    public class MyTrustManager implements X509TrustManager {
        @Override
        public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
        }

        @Override
        public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
        }

        @Override
        public X509Certificate[] getAcceptedIssuers() {
            return new X509Certificate[0];
        }
    }

    //实现HostnameVerifier接口
    private class TrustAllHostnameVerifier implements HostnameVerifier {
        @Override
        public boolean verify(String hostname, SSLSession session) {
            return true;
        }
    }

    private static RequestBody create(final MediaType contentType,
                                      final byte[] content, final int offset, final int size) {
        if (content == null) {
            throw new NullPointerException("content is null");
        }
        return new RequestBody() {
            @Override
            public MediaType contentType() {
                return contentType;
            }

            @Override
            public long contentLength() {
                return size;
            }

            @Override
            public void writeTo(BufferedSink sink) throws IOException {
                sink.write(content, offset, size);
            }
        };
    }

    public Response get(String url) {
        return get(url, null, null, 0);
    }

    public Response get(String url, Map<String, Object> param) {
        return get(url, param, null, 0);
    }

    public Response get(String url, int retry) {
        return get(url, null, null, retry);
    }

    public Response get(String url, Map<String, Object> param, int retry) {
        return get(url, param, null, retry);
    }

    public Response get(String url, Map<String, Object> param, Map<String, Object> headers) {
        return get(url, param, headers, 0);
    }

    public Response get(String url, Map<String, Object> param, Map<String, Object> headers, int retry) {
        Request.Builder requestBuilder = new Request.Builder().get();
        HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder();
        if (param != null) {
            param.forEach((s, o) -> {
                urlBuilder.addQueryParameter(s, o.toString());
            });
        }
        requestBuilder.url(urlBuilder.build());
        return send(requestBuilder, headers, retry);
    }

    public Response post(String url, byte[] body, Map<String, Object> headers) {
        return post(url, body, headers, JsonMime);
    }

    public Response post(String url, String body, Map<String, Object> headers) {
        return post(url, body.getBytes(Charset.forName("UTF-8")), headers, JsonMime);
    }

    public Response post(String url, Map<String, Object> params, Map<String, Object> headers) {
        final FormBody.Builder f = new FormBody.Builder();
        params.forEach((s, o) -> {
            f.add(s, Objects.isNull(o) ? null : o.toString());
        });
        return post(url, f.build(), headers);
    }

    public Response post(String url, byte[] body, Map<String, Object> headers, String contentType) {
        RequestBody rbody;
        if (body != null && body.length > 0) {
            MediaType t = MediaType.parse(contentType);
            rbody = RequestBody.create(t, body);
        } else {
            rbody = RequestBody.create(null, new byte[0]);
        }
        return post(url, rbody, headers);
    }

    public Response post(String url, byte[] body, int offset, int size, Map<String, Object> headers, String contentType) {
        RequestBody rbody;
        if (body != null && body.length > 0) {
            MediaType t = MediaType.parse(contentType);
            rbody = create(t, body, offset, size);
        } else {
            rbody = RequestBody.create(null, new byte[0]);
        }
        return post(url, rbody, headers);
    }

    private Response post(String url, RequestBody body, Map<String, Object> headers) {
        Request.Builder requestBuilder = new Request.Builder().url(url).post(body);
        return send(requestBuilder, headers, 0);
    }

    public Response put(String url, byte[] body, Map<String, Object> headers, String contentType) {
        RequestBody rbody;
        if (body != null && body.length > 0) {
            MediaType t = MediaType.parse(contentType);
            rbody = RequestBody.create(t, body);
        } else {
            rbody = RequestBody.create(null, new byte[0]);
        }
        return put(url, rbody, headers);
    }

    private Response put(String url, RequestBody body, Map<String, Object> headers) {
        Request.Builder requestBuilder = new Request.Builder().url(url).put(body);
        return send(requestBuilder, headers, 0);
    }

    public Response delete(String url, Map<String, Object> headers) {
        Request.Builder requestBuilder = new Request.Builder().delete().url(url);
        return send(requestBuilder, headers, 0);
    }

    public Response multipartPost(String url,
                                  Map<String, Object> fields,
                                  String name,
                                  String fileName,
                                  byte[] fileBody,
                                  String mimeType,
                                  Map<String, Object> headers) {
        RequestBody file = RequestBody.create(MediaType.parse(mimeType), fileBody);
        return multipartPost(url, fields, name, fileName, file, headers);
    }

    public Response multipartPost(String url,
                                  Map<String, Object> fields,
                                  String name,
                                  String fileName,
                                  File fileBody,
                                  String mimeType,
                                  Map<String, Object> headers) {
        RequestBody file = RequestBody.create(MediaType.parse(mimeType), fileBody);
        return multipartPost(url, fields, name, fileName, file, headers);
    }

    private Response multipartPost(String url,
                                   Map<String, Object> fields,
                                   String name,
                                   String fileName,
                                   RequestBody file,
                                   Map<String, Object> headers) {
        final MultipartBody.Builder mb = new MultipartBody.Builder();
        mb.addFormDataPart(name, fileName, file);

        fields.forEach((s, o) -> {
            mb.addFormDataPart(s, o.toString());
        });
        mb.setType(MediaType.parse("multipart/form-data"));
        RequestBody body = mb.build();
        Request.Builder requestBuilder = new Request.Builder().url(url).post(body);
        return send(requestBuilder, headers, 0);
    }

    public Response patch(String url, byte[] body, Map<String, Object> headers) {
        return patch(url, body, headers, JsonMime);
    }

    public Response patch(String url, String body, Map<String, Object> headers) {
        return patch(url, body.getBytes(Charset.forName("UTF-8")), headers, JsonMime);
    }

    public Response patch(String url, Map<String, Object> params, Map<String, Object> headers) {
        final FormBody.Builder f = new FormBody.Builder();
        params.forEach((s, o) -> {
            f.add(s, o.toString());
        });
        return patch(url, f.build(), headers);
    }

    public Response patch(String url, byte[] body, Map<String, Object> headers, String contentType) {
        RequestBody rbody;
        if (body != null && body.length > 0) {
            MediaType t = MediaType.parse(contentType);
            rbody = RequestBody.create(t, body);
        } else {
            rbody = RequestBody.create(null, new byte[0]);
        }
        return patch(url, rbody, headers);
    }

    public Response patch(String url, byte[] body, int offset, int size,
                          Map<String, Object> headers, String contentType) {
        RequestBody rbody;
        if (body != null && body.length > 0) {
            MediaType t = MediaType.parse(contentType);
            rbody = create(t, body, offset, size);
        } else {
            rbody = RequestBody.create(null, new byte[0]);
        }
        return patch(url, rbody, headers);
    }

    private Response patch(String url, RequestBody body, Map<String, Object> headers) {
        Request.Builder requestBuilder = new Request.Builder().url(url).patch(body);
        return send(requestBuilder, headers, 0);
    }


    public Response send(final Request.Builder requestBuilder, Map<String, Object> headers, int retry) {
        if (headers != null) {
            headers.forEach((s, o) -> {
                requestBuilder.header(s, o.toString());
            });
        }
        okhttp3.Response res = null;
        Response r = null;
        for (int i = 0; i < retry + 1; i++) {
            IpTag tag = new IpTag();
            try {
                res = httpClient.newCall(requestBuilder.tag(tag).build()).execute();
                r = Response.create(res, tag.ip);
            } catch (IOException e) {
                e.printStackTrace();
                r = Response.createError(tag.ip, e.getMessage(), e);
            } finally {
                if (res != null) {
                    res.close();
                }
            }
            if (!r.needRetry()) {
                break;
            }
        }
        return r;
    }

    public void asyncSend(final Request.Builder requestBuilder, Map<String, Object> headers, final AsyncCallback cb) {
        if (headers != null) {
            headers.forEach((s, o) -> {
                requestBuilder.header(s, o.toString());
            });
        }
        IpTag tag = new IpTag();
        httpClient.newCall(requestBuilder.tag(tag).build()).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                e.printStackTrace();
                cb.complete(Response.createError(tag.ip, e.getMessage(), e));
            }

            @Override
            public void onResponse(Call call, okhttp3.Response response) {
                Response r;
                try {
                    r = Response.create(response, tag.ip);
                } catch (IOException e) {
                    e.printStackTrace();
                    r = Response.createError(tag.ip, e.getMessage(), e);
                } finally {
                    response.close();
                }
                cb.complete(r);
            }
        });
    }

    public void asyncPost(String url, byte[] body, int offset, int size,
                          Map<String, Object> headers, String contentType, AsyncCallback cb) {
        RequestBody rbody;
        if (body != null && body.length > 0) {
            MediaType t = MediaType.parse(contentType);
            rbody = create(t, body, offset, size);
        } else {
            rbody = RequestBody.create(null, new byte[0]);
        }

        Request.Builder requestBuilder = new Request.Builder().url(url).post(rbody);
        asyncSend(requestBuilder, headers, cb);
    }

    public void asyncMultipartPost(String url,
                                   Map<String, Object> fields,
                                   String name,
                                   String fileName,
                                   byte[] fileBody,
                                   String mimeType,
                                   Map<String, Object> headers,
                                   AsyncCallback cb) {
        RequestBody file = RequestBody.create(MediaType.parse(mimeType), fileBody);
        asyncMultipartPost(url, fields, name, fileName, file, headers, cb);
    }

    public void asyncMultipartPost(String url,
                                   Map<String, Object> fields,
                                   String name,
                                   String fileName,
                                   File fileBody,
                                   String mimeType,
                                   Map<String, Object> headers,
                                   AsyncCallback cb) {
        RequestBody file = RequestBody.create(MediaType.parse(mimeType), fileBody);
        asyncMultipartPost(url, fields, name, fileName, file, headers, cb);
    }

    private void asyncMultipartPost(String url,
                                    Map<String, Object> fields,
                                    String name,
                                    String fileName,
                                    RequestBody file,
                                    Map<String, Object> headers,
                                    AsyncCallback cb) {
        final MultipartBody.Builder mb = new MultipartBody.Builder();
        mb.addFormDataPart(name, fileName, file);

        fields.forEach((s, o) -> {
            mb.addFormDataPart(s, o.toString());
        });
        mb.setType(MediaType.parse("multipart/form-data"));
        RequestBody body = mb.build();
        Request.Builder requestBuilder = new Request.Builder().url(url).post(body);
        asyncSend(requestBuilder, headers, cb);
    }

    private static class IpTag {
        public String ip = null;
    }
}
